﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO;
using System.Diagnostics;
using System.IO.IsolatedStorage;
using System.Runtime.Serialization;

namespace Agent.Messaging
{
  /// <summary>
  /// Implements a message queue that can be used from multipled processes. 
  /// </summary>
  /// <remarks>
  /// The queue is implemented using the filesystem and is protected via named mutex
  /// TODO: Support queues of type T, where T is any serializable object (not just strings)
  /// </remarks>
  internal class MessageQueue : IDisposable
  {
    /// <summary>
    /// List of all the currently-open queues
    /// </summary>
    static List<MessageQueue> allQueues = new List<MessageQueue>();

#if DEBUG
    static List<string> closedQueues = new List<string>();
#endif

    /// <summary>
    /// The mutex that protects the current queue
    /// </summary>
    Mutex queueMutex;

    /// <summary>
    /// The file stream that represents the stored messages
    /// </summary>
    IsolatedStorageFileStream messageStream;

    /// <summary>
    /// Whether or not the queue has been disposed
    /// </summary>
    bool isDisposed;

    static DetailedLogger staticLogger = new DetailedLogger("MessageQueue_Static");

    DetailedLogger logger;

    internal string Name { get; private set; }

    /// <summary>
    /// Opens the named queue, or creates it if it doesn't already exist
    /// </summary>
    /// <param name="queueName">The name of the queue</param>
    /// <returns>The opened or created queue</returns>
    /// <remarks>
    /// The same queue name would be used in each process that wanted to communicate
    /// </remarks>
    internal static MessageQueue OpenQueue(string queueName)
    {
      staticLogger.Log("Trying to open queue " + queueName);
      MessageQueue queue = allQueues.FirstOrDefault(x => x.Name == queueName);
      if (queue != null)
      {
        staticLogger.Log("Found queue " + queueName + ", isDisposed = " + queue.isDisposed);
        Debug.Assert(queue.isDisposed != true);
        return queue;
      }

      staticLogger.Log("Creating new queue " + queueName);

#if DEBUG
      if (closedQueues.Contains(queueName))
        staticLogger.Log("This queue existed previously " + queueName);
#endif

      queue = new MessageQueue(queueName);
      allQueues.Add(queue);
      staticLogger.Log("Added " + queueName + " to all queues and returning it");
      return queue;
    }

    /// <summary>
    /// Creates a new queue in the case that an existing one was not opened
    /// </summary>
    /// <param name="queueName">The name of the queue to create</param>
    private MessageQueue(string queueName)
    {
      logger = new DetailedLogger(queueName);
      logger.Log("Constructor", LoggerSeverity.Information);

      IsolatedStorageFile isf = null;

      try
      {
        Name = queueName;
        queueMutex = new Mutex(false, queueName + "_queue_mutex");
        isf = IsolatedStorageFile.GetUserStoreForApplication();
        messageStream = isf.OpenFile(queueName + "_message_file.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
      }
      catch
      {
        if (queueMutex != null)
          queueMutex.Dispose();
        queueMutex = null;

        if (isf != null)
          isf.Dispose();

        if (messageStream != null)
          messageStream.Dispose();
        messageStream = null;

        throw;
      }
    }

    /// <summary>
    /// Closes the queue and releases any resources associated with it
    /// </summary>
    internal void Close()
    {
      Dispose();
    }

    /// <summary>
    /// Queues up the specified message
    /// </summary>
    /// <param name="message">The message to queue up</param>
    /// <remarks>
    /// TODO: Hand off the actual writing of the message to another thread
    /// </remarks>
    internal void EnqueueMessage(string message)
    {
      if (isDisposed)
        throw new ObjectDisposedException("this");

      lock (this)
      {
        try
        {
          queueMutex.WaitOne();
          messageStream.Seek(0, SeekOrigin.End);
          StreamWriter writer = new StreamWriter(messageStream);
          writer.WriteLine(message);
          writer.Flush();
        }
        finally
        {
          queueMutex.ReleaseMutex();
        }
      }
    }

    /// <summary>
    /// Checks whether there are any messages available
    /// </summary>
    /// <returns>true if messages are available; false otherwise</returns>
    /// <remarks>
    /// TODO: Implement something better than checking the file length!
    /// </remarks>
    internal bool AreMessagesAvailable()
    {
      if (isDisposed)
        throw new ObjectDisposedException("this");

      try
      {
        queueMutex.WaitOne();
        if (messageStream.Length > 0)
          return true;

        return false;
      }
      finally
      {
        queueMutex.ReleaseMutex();
      }
    }

    /// <summary>
    /// Dequeues a single message from the queue
    /// </summary>
    /// <returns>The message that was dequeued, or null if there were no messages</returns>
    internal string DequeueMessage()
    {
      string[] results = DequeueMessageList(false);
      if (results == null || results.Length == 0)
        return null;

      return results[0];
    }

    /// <summary>
    /// Dequeues all messages from the queue
    /// </summary>
    /// <returns>The message that was dequeued, or null if there were no messages</returns>
    internal string[] DequeueAllMessages()
    {
      return DequeueMessageList(true);
    }

    /// <summary>
    /// Dequeues items from the queue
    /// </summary>
    /// <param name="retrieveAll">Specifies whether to dequeue all items (true) or only a single item (false)</param>
    /// <returns>The dequeued message(s), or null if none are waiting</returns>
    /// <remarks>
    /// TODO: Implement something better than reading all from filesystem at once
    /// </remarks>
    string[] DequeueMessageList(bool retrieveAll)
    {
      if (isDisposed)
        throw new ObjectDisposedException("this");

      string readText = null;
      string rest = null;

      try
      {
        queueMutex.WaitOne();

        if (messageStream.Length > 0)
        {
          messageStream.Seek(0, SeekOrigin.Begin);
          StreamReader reader = new StreamReader(messageStream);
          if (retrieveAll == true)
          {
            readText = reader.ReadToEnd();
            rest = "";
          }
          else
          {
            readText = reader.ReadLine();
            rest = reader.ReadToEnd();
          }
          messageStream.Seek(0, SeekOrigin.Begin);
          StreamWriter writer = new StreamWriter(messageStream);
          writer.Write(rest);
          messageStream.SetLength(rest.Length);
          writer.Flush();
        }
      }
      finally
      {
        queueMutex.ReleaseMutex();
      }

      if (readText != null)
        return readText.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

      return null;
    }

    /// <summary>
    /// Disposes of the object and all resources
    /// </summary>
    public void Dispose()
    {
      Dispose(false);
      GC.SuppressFinalize(this);
    }

    /// <summary>
    /// Disposes of the object and all resources
    /// </summary>
    /// <param name="isFinalizing">true if finalizing; false otherwise</param>
    void Dispose(bool isFinalizing)
    {
      logger.Log("isFinalizing = " + isFinalizing + ", isDisposed = " + isDisposed);
      if (isFinalizing)
        logger.Log("Exceptions are expected if some items are already disposed...");

      if (isDisposed)
      {
        logger.Log("Already disposed; returning immediately");
        return;
      }

      isDisposed = true;

      if (queueMutex != null)
      {
        logger.Log("Disposing the mutex");
        queueMutex.Dispose();
        queueMutex = null;
      }

      if (messageStream != null)
      {
        logger.Log("Disposing the message stream");
        messageStream.Dispose();
        messageStream = null;
      }

      if (allQueues.Contains(this))
      {
        logger.Log("Removing from the allQueues list");
        allQueues.Remove(this);
      }
      else
      {
        logger.Log("...never made it onto the allQueues list");
      }

#if DEBUG
      if (closedQueues.Contains(Name))
        logger.Log("Queue " + Name + " is already on the closed list; not adding again");
      else
      {
        logger.Log("Adding " + Name + " to the closed list");
        closedQueues.Add(Name);
      }
#endif
    }

    /// <summary>
    /// Finalizer
    /// </summary>
    ~MessageQueue()
    {
      logger.Log("Destructor for " + Name);
      Dispose(true);
    }

    public bool IsDisposed { get { return isDisposed; } }
  }
}
